/* * Copyright 2014 Jakub Jirutka <jakub@jirutka.cz>. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cz.jirutka.spring.exhandler; import cz.jirutka.spring.exhandler.handlers.*; import cz.jirutka.spring.exhandler.interpolators.MessageInterpolator; import cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware; import lombok.Setter; import lombok.experimental.Accessors; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.ClassUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import javax.validation.ConstraintViolationException; import java.util.HashMap; import java.util.List; import java.util.Map; import static cz.jirutka.spring.exhandler.MapUtils.putAllIfAbsent; import static lombok.AccessLevel.NONE; import static org.springframework.http.HttpStatus.*; import static org.springframework.util.StringUtils.hasText; @Setter @Accessors(fluent=true) @SuppressWarnings("unchecked") public class RestHandlerExceptionResolverBuilder { public static final String DEFAULT_MESSAGES_BASENAME = "classpath:/cz/jirutka/spring/exhandler/messages"; private final Map<Class, RestExceptionHandler> exceptionHandlers = new HashMap<>(); @Setter(NONE) // to not conflict with overloaded setter private MediaType defaultContentType; /** * The {@link ContentNegotiationManager} to use to resolve acceptable media types. * If not provided, the default instance of {@code ContentNegotiationManager} with * {@link org.springframework.web.accept.HeaderContentNegotiationStrategy HeaderContentNegotiationStrategy} * and {@link org.springframework.web.accept.FixedContentNegotiationStrategy FixedContentNegotiationStrategy} * (with {@link #defaultContentType(MediaType) defaultContentType}) will be used. */ private ContentNegotiationManager contentNegotiationManager; /** * The message body converters to use for converting an error message into HTTP response body. * If not provided, the default converters will be used (see * {@link cz.jirutka.spring.exhandler.support.HttpMessageConverterUtils#getDefaultHttpMessageConverters() * getDefaultHttpMessageConverters()}). */ private List<HttpMessageConverter<?>> httpMessageConverters; /** * The message interpolator to set into all exception handlers implementing * {@link cz.jirutka.spring.exhandler.interpolators.MessageInterpolatorAware} * interface, e.g. {@link ErrorMessageRestExceptionHandler}. Built-in exception handlers uses * {@link cz.jirutka.spring.exhandler.interpolators.SpelMessageInterpolator * SpelMessageInterpolator} by default. */ private MessageInterpolator messageInterpolator; /** * The message source to set into all exception handlers implementing * {@link org.springframework.context.MessageSourceAware MessageSourceAware} interface, e.g. * {@link ErrorMessageRestExceptionHandler}. Required for built-in exception handlers. */ private MessageSource messageSource; /** * Whether to register default exception handlers for Spring exceptions. These are registered * <i>before</i> the provided exception handlers, so you can overwrite any of the default * mappings. Default is <tt>true</tt>. */ private boolean withDefaultHandlers = true; /** * Whether to use the default (built-in) message source as a fallback to resolve messages that * the provided message source can't resolve. In other words, it sets the default message * source as a <i>parent</i> of the provided message source. Default is <tt>true</tt>. */ private boolean withDefaultMessageSource = true; public RestHandlerExceptionResolver build() { if (withDefaultMessageSource) { if (messageSource != null) { // set default message source as top parent HierarchicalMessageSource messages = resolveRootMessageSource(messageSource); if (messages != null) { messages.setParentMessageSource(createDefaultMessageSource()); } } else { messageSource = createDefaultMessageSource(); } } if (withDefaultHandlers) { // add default handlers putAllIfAbsent(exceptionHandlers, getDefaultHandlers()); } // initialize handlers for (RestExceptionHandler handler : exceptionHandlers.values()) { if (messageSource != null && handler instanceof MessageSourceAware) { ((MessageSourceAware) handler).setMessageSource(messageSource); } if (messageInterpolator != null && handler instanceof MessageInterpolatorAware) { ((MessageInterpolatorAware) handler).setMessageInterpolator(messageInterpolator); } } RestHandlerExceptionResolver resolver = new RestHandlerExceptionResolver(); resolver.setExceptionHandlers((Map) exceptionHandlers); if (httpMessageConverters != null) { resolver.setMessageConverters(httpMessageConverters); } if (contentNegotiationManager != null) { resolver.setContentNegotiationManager(contentNegotiationManager); } if (defaultContentType != null) { resolver.setDefaultContentType(defaultContentType); } resolver.afterPropertiesSet(); return resolver; } /** * The default content type that will be used as a fallback when the requested content type is * not supported. */ public RestHandlerExceptionResolverBuilder defaultContentType(MediaType mediaType) { this.defaultContentType = mediaType; return this; } /** * The default content type that will be used as a fallback when the requested content type is * not supported. */ public RestHandlerExceptionResolverBuilder defaultContentType(String mediaType) { defaultContentType( hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null ); return this; } /** * Registers the given exception handler for the specified exception type. This handler will be * also used for all the exception subtypes, when no more specific mapping is found. * * @param exceptionClass The exception type handled by the given handler. * @param exceptionHandler An instance of the exception handler for the specified exception * type or its subtypes. */ public <E extends Exception> RestHandlerExceptionResolverBuilder addHandler( Class<? extends E> exceptionClass, RestExceptionHandler<E, ?> exceptionHandler) { exceptionHandlers.put(exceptionClass, exceptionHandler); return this; } /** * Same as {@link #addHandler(Class, RestExceptionHandler)}, but the exception type is * determined from the handler. */ public <E extends Exception> RestHandlerExceptionResolverBuilder addHandler(AbstractRestExceptionHandler<E, ?> exceptionHandler) { return addHandler(exceptionHandler.getExceptionClass(), exceptionHandler); } /** * Registers {@link ErrorMessageRestExceptionHandler} for the specified exception type. * This handler will be also used for all the exception subtypes, when no more specific mapping * is found. * * @param exceptionClass The exception type to handle. * @param status The HTTP status to map the specified exception to. */ public RestHandlerExceptionResolverBuilder addErrorMessageHandler( Class<? extends Exception> exceptionClass, HttpStatus status) { return addHandler(new ErrorMessageRestExceptionHandler<>(exceptionClass, status)); } HierarchicalMessageSource resolveRootMessageSource(MessageSource messageSource) { if (messageSource instanceof HierarchicalMessageSource) { MessageSource parent = ((HierarchicalMessageSource) messageSource).getParentMessageSource(); return parent != null ? resolveRootMessageSource(parent) : (HierarchicalMessageSource) messageSource; } else { return null; } } private Map<Class, RestExceptionHandler> getDefaultHandlers() { Map<Class, RestExceptionHandler> map = new HashMap<>(); map.put( NoSuchRequestHandlingMethodException.class, new NoSuchRequestHandlingMethodExceptionHandler() ); map.put( HttpRequestMethodNotSupportedException.class, new HttpRequestMethodNotSupportedExceptionHandler() ); map.put( HttpMediaTypeNotSupportedException.class, new HttpMediaTypeNotSupportedExceptionHandler() ); map.put( MethodArgumentNotValidException.class, new MethodArgumentNotValidExceptionHandler() ); if (ClassUtils.isPresent("javax.validation.ConstraintViolationException", getClass().getClassLoader())) { map.put( ConstraintViolationException.class, new ConstraintViolationExceptionHandler() ); } addHandlerTo( map, HttpMediaTypeNotAcceptableException.class, NOT_ACCEPTABLE ); addHandlerTo( map, MissingServletRequestParameterException.class, BAD_REQUEST ); addHandlerTo( map, ServletRequestBindingException.class, BAD_REQUEST ); addHandlerTo( map, ConversionNotSupportedException.class, INTERNAL_SERVER_ERROR ); addHandlerTo( map, TypeMismatchException.class, BAD_REQUEST ); addHandlerTo( map, HttpMessageNotReadableException.class, UNPROCESSABLE_ENTITY ); addHandlerTo( map, HttpMessageNotWritableException.class, INTERNAL_SERVER_ERROR ); addHandlerTo( map, MissingServletRequestPartException.class, BAD_REQUEST ); addHandlerTo(map, Exception.class, INTERNAL_SERVER_ERROR); // this class didn't exist before Spring 4.0 try { Class clazz = Class.forName("org.springframework.web.servlet.NoHandlerFoundException"); addHandlerTo(map, clazz, NOT_FOUND); } catch (ClassNotFoundException ex) { // ignore } return map; } private void addHandlerTo(Map<Class, RestExceptionHandler> map, Class exceptionClass, HttpStatus status) { map.put(exceptionClass, new ErrorMessageRestExceptionHandler(exceptionClass, status)); } private MessageSource createDefaultMessageSource() { ReloadableResourceBundleMessageSource messages = new ReloadableResourceBundleMessageSource(); messages.setBasename(DEFAULT_MESSAGES_BASENAME); messages.setDefaultEncoding("UTF-8"); messages.setFallbackToSystemLocale(false); return messages; } }